本节代码对应 GitHub 分支: chapter9

仓库传送门 (opens new window)

# 播放器逻辑

首先,将 Player/index.js 中的获取歌词的代码完善一下。

// 记得引入插件
import Lyric from './../../api/lyric-parser';

const handleLyric = ({lineNum, txt}) => {
  if (!currentLyric.current) return;
  currentLineNum.current = lineNum;
  setPlayingLyric (txt);
};

const getLyric = id => {
  let lyric = "";
  if (currentLyric.current) {
    currentLyric.current.stop ();
  }
  // 避免 songReady 恒为 false 的情况
  getLyricRequest (id)
    .then (data => {
      lyric = data.lrc.lyric;
      if (!lyric) {
        currentLyric.current = null;
        return;
      }
      currentLyric.current = new Lyric (lyric, handleLyric);
      currentLyric.current.play ();
      currentLineNum.current = 0;
      currentLyric.current.seek (0);
    })
    .catch (() => {
      songReady.current = true;
      audioRef.current.play ();
    });
};

对于歌词功能,已经有了一个 currentLyric 对象,但同时我们还有一条即时歌词,因此要再声明一个 currentPlayingLyric 变量:

const [currentPlayingLyric, setPlayingLyric] = useState ("");

当然,还有一个记录当前行数的 currentLineNum:

const currentLineNum = useRef (0);

然后,将这些属性传递给 nornalPlayer 处理:

<NormalPlayer
  //...
  currentLyric={currentLyric.current}
  currentPlayingLyric={currentPlayingLyric}
  currentLineNum={currentLineNum.current}
></NormalPlayer>

对于歌曲播放的过程,还有两个非常重要的逻辑需要处理,一个是歌曲暂停,一个是歌曲进度更新,这两种情况,歌词都是需要跟着改变的。

歌曲暂停 / 播放:

const clickPlaying = (e, state) => {
  //...
  if (currentLyric.current) {
    currentLyric.current.togglePlay (currentTime*1000);
  }
};

歌曲进度更新:

const onProgressChange = curPercent => {
  //...
  if (currentLyric.current) {
    currentLyric.current.seek (newTime * 1000);
  }
};

# normalPlayer 中集成

先从父组件接收歌词相关的属性:

const {
  currentLineNum,
  currentPlayingLyric,
  currentLyric
} = props;

我们希望点击中间的 CD 之后切换为歌词,因此中间部分可以保存一个状态,根据它来显示不同的内容。

import Scroll from "../../../baseUI/scroll";
import { LyricContainer, LyricWrapper } from "./style";

const currentState = useRef ("");
const lyricScrollRef = useRef ();
const lyricLineRefs = useRef ([]);

// 在 Middle 组件内
<Middle ref={cdWrapperRef} onClick={toggleCurrentState}>
  <CSSTransition
    timeout={400}
    classNames="fade"
    in={currentState.current !== "lyric"}
  >
    <CDWrapper style={{visibility: currentState.current !== "lyric" ? "visible" : "hidden"}}>
      // 其余跟以前保持一致
      <p className="playing_lyric">{currentPlayingLyric}</p>
    </CDWrapper>
  </CSSTransition>
  <CSSTransition
    timeout={400}
    classNames="fade"
    in={currentState.current === "lyric"}
  >
    <LyricContainer>
      <Scroll ref={lyricScrollRef}>
        <LyricWrapper
          style={{visibility: currentState.current === "lyric" ? "visible" : "hidden"}}
          className="lyric_wrapper"
        >
          {
            currentLyric
              ? currentLyric.lines.map ((item, index) => {
              // 拿到每一行歌词的 DOM 对象,后面滚动歌词需要!
              lyricLineRefs.current [index] = React.createRef ();
              return (
                <p
                  className={`text ${
                    currentLineNum === index ? "current" : ""
                  }`}
                  key={item + index}
                  ref={lyricLineRefs.current [index]}
                >
                  {item.txt}
                </p>
              );
            })
          : <p className="text pure"> 纯音乐,请欣赏。</p>}
        </LyricWrapper>
      </Scroll>
    </LyricContainer>
  </CSSTransition>
</Middle>

对应的 style.js 中,相应的样式代码如下:

export const LyricContainer = styled.div`
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
`;
export const LyricWrapper = styled.div`
  position: absolute;
  left: 0;
  right: 0;
  width: 100%;
  box-sizing: border-box;
  text-align: center;
  p {
    line-height: 32px;
    color: rgba (255, 255, 255, 0.5);
    white-space: normal;
    font-size: ${style ["font-size-l"]};
    &.current {
      color: #fff;
    }
    &.pure {
      position: relative;
      top: 30vh;
    }
  }
`;

其中,toggleCurrentState 为改变 Middle 状态的方法,定义如下:

const toggleCurrentState = () => {
  if (currentState.current !== "lyric") {
    currentState.current = "lyric";
  } else {
    currentState.current = "";
  }
};

这个时候打开播放器,可以完整的看到歌词了,但是你滑动进度条,歌词并没有跟着动。那这是什么原因呢?

因为父组件 currentLine 已经改变,而 normalPlayer 的歌词并没有滚动到相应位置。

现在我们就来监听 currentLine 变量,当它改变时,来进行一些歌词滚动操作。

import { useEffect } from "react";

useEffect (() => {
  if (!lyricScrollRef.current) return;
  let bScroll = lyricScrollRef.current.getBScroll ();
  if (currentLineNum > 5) {
    // 保持当前歌词在第 5 条的位置
    let lineEl = lyricLineRefs.current [currentLineNum - 5].current;
    bScroll.scrollToElement (lineEl, 1000);
  } else {
    // 当前歌词行数 <=5, 直接滚动到最顶端
    bScroll.scrollTo (0, 0, 1000);
  }
}, [currentLineNum]);

现在歌词的功能就非常正常了。

不过还有一个小小的 bug,当在歌词界面退出播放器的时候,下次进来的时候并不是 CD 先进来,我们在退出播放器的时候将状态还原。

const afterLeave = () => {
  //...
  currentState.current = "";
};

到目前为止,歌词的功能就集成完毕了。从下小节开始,我们进入到搜索模块的开发。大家加油!

阅读全文